/*
 * Silicon Motion SM712 frame buffer device
 *
 * Copyright (C) 2006 Silicon Motion Technology Corp.
 * Authors:  Ge Wang, gewang@siliconmotion.com
 *	     Boyod boyod.yang@siliconmotion.com.cn
 *
 * Copyright (C) 2009 Lemote, Inc.
 * Author:   Wu Zhangjin, wuzhangjin@gmail.com
 *
 * Copyright (C) 2011 Igalia, S.L.
 * Author:   Javier M. Mellid <jmunhoz@igalia.com>
 *
 * Copyright (C) 2014 Tom Li.
 * Author:   Tom Li (Yifeng Li) <biergaizi@member.fsf.org>
 *
 * This file is subject to the terms and conditions of the GNU General Public
 * License. See the file COPYING in the main directory of this archive for
 * more details.
 *
 * Framebuffer driver for Silicon Motion SM712 chip
 */

#include <linux/fb.h>
#include <linux/screen_info.h>
#include <linux/delay.h>

#include "sm712fb_drv.h"
#include "sm712fb_accel.h"

static inline u32 bytes_to_dword(const u8 *bytes, int length)
{
	u32 dword = 0;

	switch (length) {
#ifdef __BIG_ENDIAN
	case 3:
		dword |= bytes[2] << 8;
	case 2:
		dword |= bytes[1] << 16;
	case 1:
		dword |= bytes[0] << 24;
#else
	case 3:
		dword |= bytes[2] << 16;
	case 2:
		dword |= bytes[1] << 8;
	case 1:
		dword |= bytes[0];
#endif
	}
	return dword;
}

int sm712fb_init_accel(struct sm712fb_info *fb)
{
	u8 reg;

	/* reset the 2D engine */
	sm712_write_seq(fb, 0x21, sm712_read_seq(fb, 0x21) & 0xf8);
	reg = sm712_read_seq(fb, 0x15);
	sm712_write_seq(fb, 0x15, reg | 0x30);
	sm712_write_seq(fb, 0x15, reg);

	if (sm712fb_wait(fb) != 0)
		return -1;

	sm712_write_dpr(fb, DPR_CROP_TOPLEFT_COORDS, DPR_COORDS(0, 0));

	/* same width for DPR_PITCH and DPR_SRC_WINDOW */
	sm712_write_dpr(fb, DPR_PITCH,
			DPR_COORDS(fb->fb.var.xres, fb->fb.var.xres));
	sm712_write_dpr(fb, DPR_SRC_WINDOW,
			DPR_COORDS(fb->fb.var.xres, fb->fb.var.xres));

	if (fb->fb.var.bits_per_pixel == 24) {
		sm712_write_dpr(fb, DPR_PITCH,
				DPR_COORDS(fb->fb.var.xres * 3, fb->fb.var.xres * 3));
		sm712_write_dpr(fb, DPR_SRC_WINDOW,
				DPR_COORDS(fb->fb.var.xres, fb->fb.var.xres));
		sm712_writew_dpr(fb, DPR_DE_FORMAT_SELECT, DE_FORMAT_VAL);
	}

	sm712_write_dpr(fb, DPR_BYTE_BIT_MASK, 0xffffffff);
	sm712_write_dpr(fb, DPR_COLOR_COMPARE_MASK, 0);
	sm712_write_dpr(fb, DPR_COLOR_COMPARE, 0);
	sm712_write_dpr(fb, DPR_SRC_BASE, 0);
	sm712_write_dpr(fb, DPR_DST_BASE, 0);
	sm712_write_dpr(fb, DPR_MONO_PATTERN_LOW, 0xffffffff);
	sm712_write_dpr(fb, DPR_MONO_PATTERN_HIGH, 0xffffffff);
	sm712_read_dpr(fb, DPR_DST_BASE);

	return 0;
}

int sm712fb_wait(struct sm712fb_info *fb)
{
	int i;
	u8 reg;

	sm712_read_dpr(fb, DPR_DE_CTRL);
	for (i = 0; i < 10000; i++) {
		reg = sm712_read_seq(fb, SCR_DE_STATUS);
		if ((reg & SCR_DE_STATUS_MASK) == SCR_DE_ENGINE_IDLE)
			return 0;
		udelay(1);
	}
	printk("sm712fb: 2D engine hang detected!\n");
	return -EBUSY;
}


/*
 * VGA's CRT starting address is only effective for IBM VGA mode,
 * it doesn't work for a framebuffer. It should be removed.
 */
#if 0
int sm712fb_pan_display(struct fb_var_screeninfo *var, struct fb_info *info)
{
	struct sm712fb_info *sfb = info->par;
	u32 base;

	u8 low, high, extra;

	base = (var->yoffset * info->fix.line_length
	     + (var->xoffset & ~1) * ((info->var.bits_per_pixel + 7) / 8)) >> 2;

	/*
	 * On SM712, CRT starting address is a 19-bit value, 0x0d has the low
	 * 8-bit, 0x0c has the high 8-bit, while 0x30[6:4] stores the extra
	 * 3-bit high.
	 */
	low   = (base & 0x000000ff);
	high  = (base & 0x0000ff00) >> 8;
	extra = (base & 0x00070000) >> 16;

	sm712_write_crtc(sfb, 0x0d, low);
	sm712_write_crtc(sfb, 0x0c, high);
	sm712_write_crtc(sfb, 0x30,
			(sm712_read_crtc(sfb, 0x30) & 0x8f) | (extra << 4));

	return 0;
}
#endif

void sm712fb_fillrect(struct fb_info *info, const struct fb_fillrect *rect)
{
	u32 width = rect->width, height = rect->height;
	u32 dx = rect->dx, dy = rect->dy;
	u32 color;

	struct sm712fb_info *sfb = info->par;

	if (unlikely(info->state != FBINFO_STATE_RUNNING))
		return;

	if (unlikely(rect->rop != ROP_COPY)) {
		/*
		 * It must be ROP_XOR. It's only used to combine a hardware
		 * cursor with the screen, and should never occur. Included
		 * for completeness. If one wants to implement hardware cursor
		 * (you don't, hardware only has RGB332 cursor), ROP2_XOR
		 * should be implemented here.
		 */
		cfb_fillrect(info, rect);
		return;
	}

	if ((rect->dx >= info->var.xres_virtual) ||
	    (rect->dy >= info->var.yres_virtual))
		return;

	if (info->fix.visual == FB_VISUAL_TRUECOLOR ||
	    info->fix.visual == FB_VISUAL_DIRECTCOLOR)
		color = ((u32 *) (info->pseudo_palette))[rect->color];
	else
		color = rect->color;

	if (sfb->fb.var.bits_per_pixel == 24) {
		dx *= 3;
		dy *= 3;
		width *= 3;

		/*
		 * In 24-bit color mode, SOLIDFILL will sometimes put random color stripes
		 * of garbage on the screen, it seems to be a hardware bug. Alternatively,
		 * we initialize DPR_MONO_PATTERN_LOW & HIGH with 0xffffffff (all ones,
		 * and we have already set that in sm712fb_init_accel). Since the color
		 * of this mono pattern is controlled by DPR_FG_COLOR, BITBLTing it with
		 * ROP_COPY is effectively a rectfill().
		 */
		sm712_write_dpr(sfb, DPR_FG_COLOR, color);
		sm712_write_dpr(sfb, DPR_DST_COORDS, DPR_COORDS(dx, dy));
		sm712_write_dpr(sfb, DPR_SPAN_COORDS, DPR_COORDS(width, height));
		sm712_write_dpr(sfb, DPR_DE_CTRL, DE_CTRL_START |
				DE_CTRL_ROP2_SELECT | DE_CTRL_ROP2_SRC_IS_PATTERN |
				(DE_CTRL_COMMAND_BITBLT << DE_CTRL_COMMAND_SHIFT) |
				(DE_CTRL_ROP2_COPY << DE_CTRL_ROP2_SHIFT));
	}
	else {
		sm712_write_dpr(sfb, DPR_FG_COLOR, color);
		sm712_write_dpr(sfb, DPR_DST_COORDS, DPR_COORDS(dx, dy));
		sm712_write_dpr(sfb, DPR_SPAN_COORDS, DPR_COORDS(width, height));
		sm712_write_dpr(sfb, DPR_DE_CTRL, DE_CTRL_START | DE_CTRL_ROP2_SELECT |
				(DE_CTRL_COMMAND_SOLIDFILL << DE_CTRL_COMMAND_SHIFT) |
				(DE_CTRL_ROP2_COPY << DE_CTRL_ROP2_SHIFT));
	}
	sm712fb_wait(sfb);
}

void sm712fb_copyarea(struct fb_info *info, const struct fb_copyarea *area)
{
	u32 sx = area->sx, sy = area->sy;
	u32 dx = area->dx, dy = area->dy;
	u32 height = area->height, width = area->width;
	u32 direction;

	struct sm712fb_info *sfb = info->par;

	if (unlikely(info->state != FBINFO_STATE_RUNNING))
		return;
	if ((sx >= info->var.xres_virtual) || (sy >= info->var.yres_virtual))
		return;

	if (sy < dy || (sy == dy && sx <= dx)) {
		sx += width - 1;
		dx += width - 1;
		sy += height - 1;
		dy += height - 1;
		direction = DE_CTRL_RTOL;
	} else {
		direction = 0;
	}

	if (sfb->fb.var.bits_per_pixel == 24) {
		sx *= 3;
		sy *= 3;
		dx *= 3;
		dy *= 3;
		width *= 3;
		if (direction == DE_CTRL_RTOL) {
			/*
			 * some hardware shenanigan from the original git
			 * commit, that is never clearly mentioned in the
			 * official datasheet. Not sure whether it even
			 * works correctly.
			 */
			sx += 2;
			dx += 2;
		}
	}

	sm712_write_dpr(sfb, DPR_SRC_COORDS, DPR_COORDS(sx, sy));
	sm712_write_dpr(sfb, DPR_DST_COORDS, DPR_COORDS(dx, dy));
	sm712_write_dpr(sfb, DPR_SPAN_COORDS, DPR_COORDS(width, height));
	sm712_write_dpr(sfb, DPR_DE_CTRL,
			DE_CTRL_START | DE_CTRL_ROP2_SELECT | direction |
			(DE_CTRL_COMMAND_BITBLT << DE_CTRL_COMMAND_SHIFT) |
			(DE_CTRL_ROP2_COPY << DE_CTRL_ROP2_SHIFT));
	sm712fb_wait(sfb);
}

void sm712fb_imageblit(struct fb_info *info, const struct fb_image *image)
{
	u32 dx = image->dx, dy = image->dy;
	u32 width = image->width, height = image->height;
	u32 fg_color, bg_color;

	u32 total_bytes, total_dwords, leftovers;
	u32 i;
	u32 idx = 0;
	u32 scanline = image->width >> 3;

	struct sm712fb_info *sfb = info->par;

	if (unlikely(info->state != FBINFO_STATE_RUNNING))
		return;
	if ((image->dx >= info->var.xres_virtual) ||
	    (image->dy >= info->var.yres_virtual))
		return;

	if (unlikely(image->depth != 1)) {
		/* unsupported depth, fallback to draw Tux */
		cfb_imageblit(info, image);
		return;
	}

	if (info->fix.visual == FB_VISUAL_TRUECOLOR ||
	    info->fix.visual == FB_VISUAL_DIRECTCOLOR) {
		fg_color = ((u32 *) (info->pseudo_palette))[image->fg_color];
		bg_color = ((u32 *) (info->pseudo_palette))[image->bg_color];
	} else {
		fg_color = image->fg_color;
		bg_color = image->bg_color;
	}

	/* total bytes we need to write */
	total_bytes = (width + 7) / 8;
	total_dwords = (total_bytes & ~3) / 4;
	leftovers = total_bytes & 3;

	if (sfb->fb.var.bits_per_pixel == 24) {
		dx *= 3;
		dy *= 3;
		width *= 3;
	}
	sm712_write_dpr(sfb, DPR_SRC_COORDS, 0);
	sm712_write_dpr(sfb, DPR_DST_COORDS, DPR_COORDS(dx, dy));
	sm712_write_dpr(sfb, DPR_SPAN_COORDS, DPR_COORDS(width, height));
	sm712_write_dpr(sfb, DPR_FG_COLOR, fg_color);
	sm712_write_dpr(sfb, DPR_BG_COLOR, bg_color);
	sm712_write_dpr(sfb, DPR_DE_CTRL, DE_CTRL_START | DE_CTRL_ROP2_SELECT |
			(DE_CTRL_COMMAND_HOST_WRITE << DE_CTRL_COMMAND_SHIFT) |
			(DE_CTRL_HOST_MONO << DE_CTRL_HOST_SHIFT) |
			(DE_CTRL_ROP2_COPY << DE_CTRL_ROP2_SHIFT));

	for (i = 0; i < height; i++) {
		iowrite32_rep(sfb->dataport, &image->data[idx], total_dwords);
		if (leftovers) {
			writel_relaxed(
				bytes_to_dword(
					&image->data[idx + total_dwords * 4],
					leftovers),
				sfb->dataport);
		}
		idx += scanline;
	}
	mb();
	sm712fb_wait(sfb);
}
